package com.datalayer.utils;

import com.datalayer.annotations.*;
import com.datalayer.utils.builder.Builder;
import com.datalayer.utils.builder.ClassBuilder;
import com.datalayer.utils.builder.ObjectBuilder;
import com.datalayer.utils.builder.ResolveReferencesDirector;

import java.lang.annotation.Annotation;
import java.lang.annotation.AnnotationTypeMismatchException;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * User: Georgiana.Scarlat
 * Date: 7/6/12
 * Time: 4:41 PM
 */
public class OrmProcessing {

    /**
     * Obtains a primary key constraint.
     *
     * @param pkeys List of primary Keys.
     * @return the primary key constarint as a String to be added in the "create table ..." SQL command
     */
    public static String getPrimaryKeyConstraint(List<String> pkeys) {

        if (pkeys.size() == 0)
            return "";

        String constraint = "primary key (";

        for (String key : pkeys) {
            constraint += key + ", ";
        }

        constraint = constraint.substring(0, constraint.length() - 2) + "), ";
        return constraint;
    }

    /**
     * Obtains the value of an Annotation.
     *
     * @param type           1 - annotation of a class, 2 - annotation of a method
     * @param class_method   the class or method that is annotated
     * @param annotationType the class of the annotation
     * @param fieldName      the field of the annotation whose value we want to obtain
     * @param <T>            Either Class or Method
     * @return the value of the annotation field specified
     * @throws NoSuchMethodException
     * @throws java.lang.reflect.InvocationTargetException
     *
     * @throws IllegalAccessException
     */
    @SuppressWarnings("unchecked")
    public static <T> Object getAnnotationValue(int type, T class_method, Class annotationType, String fieldName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Object value = null;

        Annotation annotation = null;

        switch (type) {
            case 1:
                annotation = ((Class) class_method).getAnnotation(annotationType);
                break;
            case 2:
                annotation = ((Method) class_method).getAnnotation(annotationType);
                break;

        }

        if (annotation != null) {

            annotation.annotationType().getMethod(fieldName).setAccessible(true);
            value = annotation.annotationType().getMethod(fieldName).invoke(annotation);


        }

        return value;

    }


    /**
     * Obtains the Column name and type of a field from a class that is mapped to a table.
     *
     * @param mappedClass the class that is mapped to a table
     * @param fieldName   the field name from a class
     * @return a Column object containing the column name, type and constraint
     *
     */
    public static Column columnNameInTable(Class mappedClass, String fieldName) throws AnnotationTypeMismatchException {
        Column column = null;
        String name, type, constraint;

        Method methods[] = mappedClass.getDeclaredMethods();


        for (Method m : methods) {

            try {

                String field = (String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "fieldName");

                if (field != null && field.equals(fieldName)) {

                    name = (String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "attributeName");
                    type = (String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "type");
                    constraint = (String) OrmProcessing.getAnnotationValue(2, m, Constraint.class, "constraintName");
                    column = new Column(name, type, constraint);
                    break;

                }

            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            }

        }

        return column;


    }

    /**
     * Obtains the column name of a field that references another class (foreign key).
     *
     * @param mappedClass the class that is mapped to a table
     * @param fieldName   the name of the field from the specified class
     * @return the column name
     */
    public static String foreignColumnNameInTable(Class mappedClass, String fieldName) throws IncompleteAnnotationException {

        String name = "";


        Method methods[] = mappedClass.getDeclaredMethods();

        try {

            for (Method m : methods) {
                Annotation annotation = m.getAnnotation(Association.class);
                if (annotation != null) {

                    Join[] joins = (Join[]) annotation.annotationType().getMethod("joins").invoke(annotation);

                    for (Join join : joins) {

                        if (join.fieldName().equals(fieldName)) {

                            name = join.insideName();

                        }
                    }
                }
            }

            return name;
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Association.class, "joins");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Association.class, "joins");
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Association.class, "joins");
        }

    }

    /**
     * Obtains a hashmap  of a foreign key column name and the column name that it refers in the foreign table.
     *
     * @param foreignClass the foreign class that is referred by the specified method
     * @param method       the method that refers a foreign class
     * @return a hashmap of a foreign key column name and the column name that it refers in the foreign table
     *
     */
    public static HashMap<String, Column> joinReferences(Class foreignClass, Method method) throws AnnotationTypeMismatchException, IncompleteAnnotationException {

        HashMap<String, Column> reference = new HashMap<String, Column>();
        Annotation annotation = method.getAnnotation(Association.class);
        if (annotation == null) return reference;

        Join[] joins;

        try {

            joins = (Join[]) annotation.annotationType().getMethod("joins").invoke(annotation);

        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Association.class, "joins");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Association.class, "joins");
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Association.class, "joins");
        }


        for (Join join : joins) {

            if (!join.insideName().equals("")) {
                reference.put(join.insideName(), columnNameInTable(foreignClass, join.foreignName()));
            }
        }

        return reference;
    }

    /**
     * Obtains the foreign key constraint needed for the 'create table...' SQL command.
     *
     * @param tableName the table name
     * @param hashMap   a hashmap of foreign key column name as key and Column object reresenting the referened column as value
     * @return the foreign key constraint as used in the 'create tale...' syntax
     */
    public static String getReferenceConstraint(String tableName, HashMap<String, Column> hashMap) {
        String constraint = "";

        if (hashMap.size() == 0)
            return constraint;

        constraint = "FOREIGN KEY (";
        for (String fk : hashMap.keySet()) {
            constraint += fk + ", ";
        }

        constraint = constraint.substring(0, constraint.length() - 2) + ") REFERENCES " + tableName + "(";

        for (Column pk : hashMap.values()) {
            constraint += pk.getName() + ", ";
        }

        constraint = constraint.substring(0, constraint.length() - 2) + ")" + ", ";


        return constraint;
    }


    /**
     * Obtains a list of objects that have foreign references towards the current object.
     * <p/>
     * Thus they depend on this object, so they have to be deleted/updated
     * before deleting/updating this object.
     *
     * @param object the current object
     * @param source the object from which this object was found (used to avoid infinite recursion)
     * @return list of objects that have foreign references towards the current object
     */

    @SuppressWarnings("unchecked")
    public static <T> List<T> getReferencedObjects(Object object, Object source) throws UnsupportedOperationException, AnnotationTypeMismatchException, IncompleteAnnotationException {

        List<T> ro = new LinkedList<T>();
        boolean class_or_obj = false;
        if (object instanceof Class) class_or_obj = true;
        Method methods[] = class_or_obj ? ((Class) object).getDeclaredMethods() : object.getClass().getDeclaredMethods();


        for (Method m : methods) {
            try {

                String direction = (String) getAnnotationValue(2, m, Direction.class, "value");
                if ("get".equals(direction) && m.isAnnotationPresent(Association.class)) {
                    AssociationType type = (AssociationType) getAnnotationValue(2, m, Association.class, "type");

                    Builder builder = class_or_obj ? new ClassBuilder(m, (Class) object, (Class) source) : new ObjectBuilder(m, object, source);
                    ResolveReferencesDirector resolveReferencesDirector = new ResolveReferencesDirector(builder);
                    ro.addAll(resolveReferencesDirector.<T>getReferences(type));

                }
            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            }

        }
        return ro;


    }

    /**
     * Obtains a list of referenced classes within a given class.
     * The classes are referenced through a collection.
     * (many2many or many2one relationships)
     *
     * @param cls    current class
     * @param source the class which referenced the current class or null
     * @return a list of referenced classes within a given class
     *
     */
    public static List<Class> getReferencedCollectionClasses(Class cls, Class source) throws IncompleteAnnotationException {
        List<Class> result = new LinkedList<Class>();
        Method methods[] = cls.getDeclaredMethods();

        for (Method m : methods) {
            String direction;
            try {
                direction = (String) getAnnotationValue(2, m, Direction.class, "value");
            } catch (NoSuchMethodException e) {
                throw new IncompleteAnnotationException(Direction.class, "value");
            } catch (InvocationTargetException e) {
                throw new IncompleteAnnotationException(Direction.class, "value");
            } catch (IllegalAccessException e) {
                throw new IncompleteAnnotationException(Direction.class, "value");
            }
            if ("add".equals(direction) && m.isAnnotationPresent(Association.class)) {
                Class c = m.getParameterTypes()[0];
                if (!c.equals(source))
                    result.add(c);

            }
        }
        return result;


    }

    /**
     * Obtains the name of the field the method refers.
     *
     * @param method the method
     * @return the name of the field the method refers
     *
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    @SuppressWarnings("UnusedDeclaration")
    public static String getAssociatedFieldName(Method method) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        String fieldName = null;

        Annotation annotation = method.getAnnotation(Association.class);
        if (annotation == null) return fieldName;

        Join[] joins = (Join[]) annotation.annotationType().getMethod("joins").invoke(annotation);


        fieldName = joins[0].fieldName();


        return fieldName;
    }

    /**
     * Obtains the value of a column from an object of a class mapped to a table.
     *
     * @param entity     object of a class mapped to a table
     * @param columnName the column name
     * @return the value of the column (if it is of type String the it is wrapped in '')
     */
    public static Object getColumnValueByName(Object entity, String columnName) throws AnnotationTypeMismatchException {

        Object result = null;
        Class foreignClass = entity.getClass();
        Method methods[] = foreignClass.getDeclaredMethods();

        for (Method m : methods) {
            try {

                String direction = (String) getAnnotationValue(2, m, Direction.class, "value");

                if (m.isAnnotationPresent(Attribute.class) && "get".equals(direction)) {
                    String field = (String) getAnnotationValue(2, m, Attribute.class, "attributeName");
                    if (field.equals(columnName)) {

                        m.setAccessible(true);
                        result = m.invoke(entity);

                        if (m.getReturnType().equals(String.class))
                            result = "'" + result + "'";

                        break;

                    }
                }

            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is either not annotated Correctly or can't be invoked.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is either not annotated Correctly or can't be invoked.");
            }

        }

        return result;
    }

    /**
     * Transforms a hashmap of keys and values to a list of Strings "key = value".
     *
     * @param eqRef hashmap
     * @return list of Strings "key = value"
     */
    public static List<String> addEqual(HashMap<String, Object> eqRef) {
        List<String> result = new LinkedList<String>();

        for (Map.Entry<String, Object> entry : eqRef.entrySet()) {
            result.add(entry.getKey() + "=" + entry.getValue().toString());
        }
        return result;
    }


    /**
     * Obtains the clause from  the list of parts where the entities are declared and maybe aliased.
     *
     * @param parts list of parts(lists of words in a clause)
     * @return list of words in declaring clause
     */
    public static List<String> getDeclaringClause(List<List<String>> parts) {
        String first_part;

        for (List<String> part : parts) {
            first_part = part.get(0).toUpperCase();
            if (first_part.equals("FROM") || first_part.equals("UPDATE")) {
                return part;
            }
        }
        return null;
    }


    /**
     * Gets a list of pairs containing class names and aliases from a MyQuery declaring clause.
     *
     * @param declaringClause the declaring clause
     * @return ist of pairs containing class names and aliases from a MyQuery declaring clause
     */
    public static List<Pair<String, String>> getNameAndAlias(List<String> declaringClause) {
        List<Pair<String, String>> class_alias = new LinkedList<Pair<String, String>>();
        Pair<String, String> aliased = null;

        for (String elem : declaringClause) {
            if (elem.equals(",") || elem.toUpperCase().equals("FROM") || elem.toUpperCase().equals("UPDATE")) {

                if (aliased != null) class_alias.add(aliased);
                aliased = new Pair<String, String>();

            } else if (aliased != null)
                aliased.add(elem);

        }
        class_alias.add(aliased);
        return class_alias;
    }

    /**
     * In an *.fieldname  word, splits the table name from the fieldname.
     *
     * @param word the name.fieldname word
     * @return a Pair of name and field name
     */
    public static Pair<String, String> splitFieldFroName(String word) {
        String pa[] = word.split("\\.");

        String first = "", second;
        int i;

        if (pa.length < 2)
            return new Pair<String, String>("", "");

        for (i = 0; i < pa.length - 1; i++)
            first += pa[i] + ".";

        first = first.substring(0, first.length() - 1);
        second = pa[i];

        return new Pair<String, String>(first, second);

    }

    /**
     * Replace class's field name with the corresponding column name.
     *
     * @param word  word in which field name appears
     * @param assoc the association between class, class name, alias and coresponding table name
     * @return the word in which the field name and class name are replaced with column name and tablename
     */
    public static String replaceFieldINQuery(String word, Map.Entry<Pair<String, String>, Pair<String, Class>> assoc) throws IncompleteAnnotationException, IllegalArgumentException {

        String newPart = null, name;
        Pair<String, String> name_field = splitFieldFroName(word);

        if (assoc.getKey().contains(name_field.getFirst())) {

            Column col = columnNameInTable(assoc.getValue().getSecond(), name_field.getSecond());

            if (col == null) {
                name = foreignColumnNameInTable(assoc.getValue().getSecond(), name_field.getSecond());
                if (name.equals(""))
                    throw new IllegalArgumentException("No suh field in class " + assoc.getValue().getSecond().getName());

            } else name = col.getName();

            if (assoc.getKey().getSecond() != null)

                newPart = assoc.getKey().getSecond() + "." + name;
            else

                newPart = assoc.getValue().getFirst() + "." + name;
        }
        return newPart;
    }


    /**
     * Replaces class names and filed names in a MySQL query with the corresponding
     * table names and column names using an name associations hashmap.
     *
     * @param associations ame associations hashmap
     * @param allParts     MySQL query in the form of a list of words
     * @return the resulting SQL query as a list of words
     */
    public static List<String> replaceInQuery(HashMap<Pair<String, String>, Pair<String, Class>> associations, List<String> allParts) throws IncompleteAnnotationException, IllegalArgumentException {

        List<String> newParts = new LinkedList<String>();
        String newPart;
        int ok;
        for (String word : allParts) {
            ok = 0;
            for (Map.Entry<Pair<String, String>, Pair<String, Class>> assoc : associations.entrySet()) {

                if (assoc.getKey().getFirst().equals(word)) {
                    ok = 1;
                    newParts.add(assoc.getValue().getFirst());
                } else {
                    newPart = replaceFieldINQuery(word, assoc);
                    if (newPart != null) {
                        ok = 1;
                        newParts.add(newPart);
                    }
                }
            }
            if (ok == 0)
                newParts.add(word);
        }

        return newParts;
    }


    /**
     * Obtains associations between column indices , column name, type and the table that it comes from.
     *
     * @param resultSet the result set from which we obtain the columns
     * @return a hashmap representing the association
     *
     */
    public static HashMap<Integer, Pair<Pair<String, String>, String>> getTableColumnAssociation(ResultSet resultSet) throws UnsupportedOperationException {

        HashMap<Integer, Pair<Pair<String, String>, String>> association = new HashMap<Integer, Pair<Pair<String, String>, String>>();

        try {
            int column_no = resultSet.getMetaData().getColumnCount();
            for (int i = 1; i <= column_no; i++) {
                Pair<String, String> name_type = new Pair<String, String>(resultSet.getMetaData().getColumnName(i), resultSet.getMetaData().getColumnClassName(i));
                Pair<Pair<String, String>, String> pair = new Pair<Pair<String, String>, String>(name_type, resultSet.getMetaData().getTableName(i));
                association.put(i, pair);
            }

            return association;
        } catch (SQLException e) {
            throw new UnsupportedOperationException("Can't process query results.",e);
        }

    }

    /**
     * Obtains the associations of column indices, column name ant type that correspond to a certain class.
     *
     * @param cls          class
     * @param associations all associations
     * @return associations of column indices, column name ant type that correspond to a certain class
     */
    public static HashMap<Pair<String, String>, Integer> getOwnColumIndexes(Class cls, HashMap<Integer, Pair<Pair<String, String>, String>> associations) throws IncompleteAnnotationException {
        HashMap<Pair<String, String>, Integer> indexes = new HashMap<Pair<String, String>, Integer>();

        try {

            String tableName = (String) getAnnotationValue(1, cls, Entity.class, "tableName");

            for (Map.Entry<Integer, Pair<Pair<String, String>, String>> entry : associations.entrySet()) {

                if (entry.getValue().getSecond().toLowerCase().equals(tableName.toLowerCase())) {
                    indexes.put(entry.getValue().getFirst(), entry.getKey());
                }
            }
            return indexes;
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        }

    }

    /**
     * For a certain object, it sets it's field identified by a column name to a certain value.
     *
     * @param object     object
     * @param name_value the name and value of a column
     */
    public static void setColumnValueForObject(Object object, Pair<String, Object> name_value) throws AnnotationTypeMismatchException {

        Method methods[] = object.getClass().getDeclaredMethods();

        for (Method m : methods) {
            try {

                String direction = (String) getAnnotationValue(2, m, Direction.class, "value");
                if ("set".equals(direction)) {
                    if (m.isAnnotationPresent(Attribute.class)) {
                        String attrVal = (String) getAnnotationValue(2, m, Attribute.class, "attributeName");
                        if (name_value.getFirst().toLowerCase().equals(attrVal.toLowerCase())) {
                            m.setAccessible(true);
                            m.invoke(object, name_value.getSecond());
                        }
                    }
                }
            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is either not annotated Correctly or can't be invoked.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is either not annotated Correctly or can't be invoked.");
            }

        }

    }


}

